---
layout: example
category: example
title: "Filter features within map view"
description: "Move the map to query viewable features in a vector tile layer and filter by typing in an input"
tags:
  - user-interaction
---
<style>
#map {
    position:absolute;
    left:25%;
    top:0;
    bottom:0;
    width: 75%;
}
.map-overlay {
    position: absolute;
    width: 25%;
    top: 0;
    bottom: 0;
    left: 0;
    font: 12px/20px 'Helvetica Neue', Arial, Helvetica, sans-serif;
    background-color: #fff;
    max-height: 100%;
    overflow: hidden;
}

.map-overlay fieldset {
    display: none;
    background: #ddd;
    border: none;
    padding: 10px;
    margin: 0;
}

.map-overlay input {
    display: block;
    border: none;
    width: 100%;
    border-radius: 3px;
    padding: 10px;
    margin: 0;
}

.map-overlay .listing {
    overflow: auto;
    max-height: 100%;
}

.map-overlay .listing > * {
    display: block;
    padding: 5px 10px;
    margin: 0;
}

.map-overlay .listing a {
    border-bottom: 1px solid rgba(0, 0, 0, 0.1);
    color: #404;
    text-decoration: none;
}

.map-overlay .listing a:last-child {
    border: none;
}

.map-overlay .listing a:hover {
    background: #f0f0f0;
}
</style>

<div id='map'></div>

<div class='map-overlay'>
    <fieldset>
        <input id='feature-filter' type='text' placeholder='Filter results by name' />
    </fieldset>
    <div id='feature-listing' class='listing'></div>
</div>

<script>
var map = new mapboxgl.Map({
    container: 'map',
    style: 'mapbox://styles/mapbox/streets-v9',
    center: [-98, 38.88],
    maxZoom: 5,
    minZoom: 1,
    zoom: 3
});

// Holds visible airport features for filtering
var airports = [];

// Create a popup, but don't add it to the map yet.
var popup = new mapboxgl.Popup({
    closeButton: false
});

var filterEl = document.getElementById('feature-filter');
var listingEl = document.getElementById('feature-listing');

function renderListings(features) {
    // Clear any existing listings
    listingEl.innerHTML = '';
    if (features.length) {
        features.forEach(function(feature) {
            var prop = feature.properties;
            var item = document.createElement('a');
            item.href = prop.wikipedia;
            item.target = '_blank';
            item.textContent = prop.name + ' (' + prop.abbrev + ')';
            item.addEventListener('mouseover', function() {
                // Highlight corresponding feature on the map
                popup.setLngLat(feature.geometry.coordinates)
                    .setText(feature.properties.name + ' (' + feature.properties.abbrev + ')')
                    .addTo(map);
            });
            listingEl.appendChild(item);
        });

        // Show the filter input
        filterEl.parentNode.style.display = 'block';
    } else {
        var empty = document.createElement('p');
        empty.textContent = 'Drag the map to populate results';
        listingEl.appendChild(empty);

        // Hide the filter input
        filterEl.parentNode.style.display = 'none';

        // remove features filter
        map.setFilter('airport', ['has', 'abbrev']);
    }
}

function normalize(string) {
    return string.trim().toLowerCase();
}

function getUniqueFeatures(array, comparatorProperty) {
    var existingFeatureKeys = {};
    // Because features come from tiled vector data, feature geometries may be split
    // or duplicated across tile boundaries and, as a result, features may appear
    // multiple times in query results.
    var uniqueFeatures = array.filter(function(el) {
        if (existingFeatureKeys[el.properties[comparatorProperty]]) {
            return false;
        } else {
            existingFeatureKeys[el.properties[comparatorProperty]] = true;
            return true;
        }
    });

    return uniqueFeatures;
}

map.on('load', function() {

    map.addLayer({
        "id": "airport",
        "source": {
            "type": "vector",
            "url": "mapbox://mapbox.04w69w5j"
        },
        "source-layer": "ne_10m_airports",
        "type": "symbol",
        "layout": {
            "icon-image": "airport-15",
            "icon-padding": 0,
            "icon-allow-overlap":true
        }
    });

    map.on('moveend', function() {
        var features = map.queryRenderedFeatures({layers:['airport']});

        if (features) {
            var uniqueFeatures = getUniqueFeatures(features, "iata_code");
            // Populate features for the listing overlay.
            renderListings(uniqueFeatures);

            // Clear the input container
            filterEl.value = '';

            // Store the current features in sn `airports` variable to
            // later use for filtering on `keyup`.
            airports = uniqueFeatures;
        }
    });

    map.on('mousemove', function(e) {
        var features = map.queryRenderedFeatures(e.point, {
            layers: ['airport']
        });

        // Change the cursor style as a UI indicator.
        map.getCanvas().style.cursor = features.length ? 'pointer' : '';

        if (!features.length) {
            popup.remove();
            return;
        }

        var feature = features[0];
        // Populate the popup and set its coordinates
        // based on the feature found.
        popup.setLngLat(feature.geometry.coordinates)
            .setText(feature.properties.name + ' (' + feature.properties.abbrev + ')')
            .addTo(map);
    });

    filterEl.addEventListener('keyup', function(e) {
        var value = normalize(e.target.value);

        // Filter visible features that don't match the input value.
        var filtered = airports.filter(function(feature) {
            var name = normalize(feature.properties.name);
            var code = normalize(feature.properties.abbrev);
            return name.indexOf(value) > -1 || code.indexOf(value) > -1;
        });

        // Populate the sidebar with filtered results
        renderListings(filtered);

        // Set the filter to populate features into the layer.
        map.setFilter('airport', ['in', 'abbrev'].concat(filtered.map(function(feature) {
            return feature.properties.abbrev;
        })));
    });

    // Call this function on initialization
    // passing an empty array to render an empty state
    renderListings([]);
});
</script>
